/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.phoenix.schema.types;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.Format;

import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.TypeMismatchException;
import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.DateUtil;
import org.apache.phoenix.util.NumberUtil;

import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions;

public class PDecimal extends PRealNumber<BigDecimal> {

    public static final PDecimal INSTANCE = new PDecimal();

    private static final BigDecimal MIN_DOUBLE_AS_BIG_DECIMAL =
            BigDecimal.valueOf(-Double.MAX_VALUE);
    private static final BigDecimal MAX_DOUBLE_AS_BIG_DECIMAL =
            BigDecimal.valueOf(Double.MAX_VALUE);
    private static final BigDecimal MIN_FLOAT_AS_BIG_DECIMAL =
            BigDecimal.valueOf(-Float.MAX_VALUE);
    private static final BigDecimal MAX_FLOAT_AS_BIG_DECIMAL =
            BigDecimal.valueOf(Float.MAX_VALUE);

    private PDecimal() {
        super("DECIMAL", Types.DECIMAL, BigDecimal.class, null, 8);
    }

    @Override
    public byte[] toBytes(Object object) {
        if (object == null) {
            return ByteUtil.EMPTY_BYTE_ARRAY;
        }
        BigDecimal v = (BigDecimal) object;
        v = NumberUtil.normalize(v);
        int len = getLength(v);
        byte[] result = new byte[Math.min(len, MAX_BIG_DECIMAL_BYTES)];
        PDataType.toBytes(v, result, 0, len);
        return result;
    }

    @Override
    public int toBytes(Object object, byte[] bytes, int offset) {
        if (object == null) {
            return 0;
        }
        BigDecimal v = (BigDecimal) object;
        v = NumberUtil.normalize(v);
        int len = getLength(v);
        return PDataType.toBytes(v, bytes, offset, len);
    }

    private int getLength(BigDecimal v) {
        int signum = v.signum();
        if (signum == 0) { // Special case for zero
            return 1;
        }
        /*
         * Size of DECIMAL includes:
         * 1) one byte for exponent
         * 2) one byte for terminal byte if negative
         * 3) one byte for every two digits with the following caveats:
         *    a) add one to round up in the case when there is an odd number of digits
         *    b) add one in the case that the scale is odd to account for 10x of lowest significant digit
         *       (basically done to increase the range of exponents that can be represented)
         */
        return (signum < 0 ? 2 : 1) + (v.precision() + 1 + (v.scale() % 2 == 0 ? 0 : 1)) / 2;
    }

    @Override
    public int estimateByteSize(Object o) {
        if (o == null) {
            return 1;
        }
        BigDecimal v = (BigDecimal) o;
        // TODO: should we strip zeros and round here too?
        return Math.min(getLength(v), MAX_BIG_DECIMAL_BYTES);
    }

    @Override
    public Integer getMaxLength(Object o) {
        if (o == null) {
            return MAX_PRECISION;
        }
        BigDecimal v = (BigDecimal) o;
        return v.precision();
    }

    @Override
    public Integer getScale(Object o) {
        return null;
    }

    @Override
    public Object toObject(byte[] b, int o, int l, PDataType actualType, SortOrder sortOrder,
            Integer maxLength, Integer scale) {
        Preconditions.checkNotNull(sortOrder);
        if (l == 0) {
            return null;
        }
        if (actualType == PDecimal.INSTANCE) {
            if (sortOrder == SortOrder.DESC) {
                b = SortOrder.invert(b, o, new byte[l], 0, l);
                o = 0;
            }
            return toBigDecimal(b, o, l);
        } else if (equalsAny(actualType, PDate.INSTANCE, PTime.INSTANCE, PUnsignedDate.INSTANCE,
                PUnsignedTime.INSTANCE, PLong.INSTANCE, PUnsignedLong.INSTANCE, PInteger.INSTANCE,
                PUnsignedInt.INSTANCE, PSmallint.INSTANCE, PUnsignedSmallint.INSTANCE, PTinyint.INSTANCE,
                PUnsignedTinyint.INSTANCE)) {
            return BigDecimal.valueOf(actualType.getCodec().decodeLong(b, o, sortOrder));
        } else if (equalsAny(actualType, PFloat.INSTANCE, PUnsignedFloat.INSTANCE)) {
            return BigDecimal.valueOf(actualType.getCodec().decodeFloat(b, o, sortOrder));
        } else if (equalsAny(actualType, PDouble.INSTANCE, PUnsignedDouble.INSTANCE)) {
            return BigDecimal.valueOf(actualType.getCodec().decodeDouble(b, o, sortOrder));
        } else if (equalsAny(actualType, PTimestamp.INSTANCE, PUnsignedTimestamp.INSTANCE)) {
            long millisPart = DateUtil.getCodecFor(actualType).decodeLong(b, o, sortOrder);
            int nanoPart = PUnsignedInt.INSTANCE.getCodec().decodeInt(b, o + Bytes.SIZEOF_LONG, sortOrder);
            BigDecimal nanosPart = BigDecimal.valueOf(
                    (nanoPart % QueryConstants.MILLIS_TO_NANOS_CONVERTOR)
                    / QueryConstants.MILLIS_TO_NANOS_CONVERTOR);
            return BigDecimal.valueOf(millisPart).add(nanosPart);
        } else if (actualType == PBoolean.INSTANCE) {
            return (Boolean) PBoolean.INSTANCE.toObject(b, o, l, actualType, sortOrder) ?
                    BigDecimal.ONE :
                        BigDecimal.ZERO;
        }
        return throwConstraintViolationException(actualType, this);
    }

    @Override
    public Object toObject(Object object, PDataType actualType) {
        if (object == null) {
            return null;
        }
        if (equalsAny(actualType, PInteger.INSTANCE, PUnsignedInt.INSTANCE)) {
            return BigDecimal.valueOf((Integer) object);
        } else if (equalsAny(actualType, PLong.INSTANCE, PUnsignedLong.INSTANCE)) {
            return BigDecimal.valueOf((Long) object);
        } else if (equalsAny(actualType, PSmallint.INSTANCE, PUnsignedSmallint.INSTANCE)) {
            return BigDecimal.valueOf((Short) object);
        } else if (equalsAny(actualType, PTinyint.INSTANCE, PUnsignedTinyint.INSTANCE)) {
            return BigDecimal.valueOf((Byte) object);
        } else if (equalsAny(actualType, PFloat.INSTANCE, PUnsignedFloat.INSTANCE)) {
            return BigDecimal.valueOf((Float) object);
        } else if (equalsAny(actualType, PDouble.INSTANCE, PUnsignedDouble.INSTANCE)) {
            return BigDecimal.valueOf((Double) object);
        } else if (actualType == PDecimal.INSTANCE) {
            return object;
        } else if (equalsAny(actualType, PDate.INSTANCE, PUnsignedDate.INSTANCE, PTime.INSTANCE,
                PUnsignedTime.INSTANCE)) {
            java.util.Date d = (java.util.Date) object;
            return BigDecimal.valueOf(d.getTime());
        } else if (equalsAny(actualType, PTimestamp.INSTANCE,
                PUnsignedTimestamp.INSTANCE)) {
            Timestamp ts = (Timestamp) object;
            long millisPart = ts.getTime();
            BigDecimal nanosPart = BigDecimal.valueOf(
                    (ts.getNanos() % QueryConstants.MILLIS_TO_NANOS_CONVERTOR)
                    / QueryConstants.MILLIS_TO_NANOS_CONVERTOR);
            BigDecimal value = BigDecimal.valueOf(millisPart).add(nanosPart);
            return value;
        } else if (actualType == PBoolean.INSTANCE) {
            return ((Boolean) object) ? BigDecimal.ONE : BigDecimal.ZERO;
        }
        return throwConstraintViolationException(actualType, this);
    }

    @Override
    public boolean isFixedWidth() {
        return false;
    }

    @Override
    public Integer getByteSize() {
        return MAX_BIG_DECIMAL_BYTES;
    }

    @Override
    public int compareTo(Object lhs, Object rhs, PDataType rhsType) {
        if (lhs == rhs) {
            return 0;
        }
        if (lhs == null) {
            return -1;
        }
        if (rhs == null) {
            return 1;
        }
        if (rhsType == PDecimal.INSTANCE) {
            return ((BigDecimal) lhs).compareTo((BigDecimal) rhs);
        }
        return -rhsType.compareTo(rhs, lhs, this);
    }

    @Override
    public boolean isCastableTo(PDataType targetType) {
        return super.isCastableTo(targetType) || targetType.isCoercibleTo(
                PTimestamp.INSTANCE) || targetType.equals(PBoolean.INSTANCE);
    }

    @Override
    public boolean isCoercibleTo(PDataType targetType, Object value) {
        if (value != null) {
            BigDecimal bd;
            if (equalsAny(targetType, PUnsignedLong.INSTANCE, PUnsignedInt.INSTANCE,
                    PUnsignedSmallint.INSTANCE, PUnsignedTinyint.INSTANCE)) {
                bd = (BigDecimal) value;
                if (bd.signum() == -1) {
                    return false;
                }
            } else if (targetType.equals(PLong.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    bd.longValueExact();
                    return true;
                } catch (ArithmeticException e) {
                    return false;
                }
            } else if (targetType.equals(PInteger.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    bd.intValueExact();
                    return true;
                } catch (ArithmeticException e) {
                    return false;
                }
            } else if (targetType.equals(PSmallint.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    bd.shortValueExact();
                    return true;
                } catch (ArithmeticException e) {
                    return false;
                }
            } else if (targetType.equals(PTinyint.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    bd.byteValueExact();
                    return true;
                } catch (ArithmeticException e) {
                    return false;
                }
            } else if (targetType.equals(PUnsignedFloat.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    BigDecimal maxFloat = MAX_FLOAT_AS_BIG_DECIMAL;
                    boolean isNegtive = (bd.signum() == -1);
                    return bd.compareTo(maxFloat) <= 0 && !isNegtive;
                } catch (Exception e) {
                    return false;
                }
            } else if (targetType.equals(PFloat.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    BigDecimal maxFloat = MAX_FLOAT_AS_BIG_DECIMAL;
                    // Float.MIN_VALUE should not be used here, as this is the
                    // smallest in terms of closest to zero.
                    BigDecimal minFloat = MIN_FLOAT_AS_BIG_DECIMAL;
                    return bd.compareTo(maxFloat) <= 0 && bd.compareTo(minFloat) >= 0;
                } catch (Exception e) {
                    return false;
                }
            } else if (targetType.equals(PUnsignedDouble.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    BigDecimal maxDouble = MAX_DOUBLE_AS_BIG_DECIMAL;
                    boolean isNegtive = (bd.signum() == -1);
                    return bd.compareTo(maxDouble) <= 0 && !isNegtive;
                } catch (Exception e) {
                    return false;
                }
            } else if (targetType.equals(PDouble.INSTANCE)) {
                bd = (BigDecimal) value;
                try {
                    BigDecimal maxDouble = MAX_DOUBLE_AS_BIG_DECIMAL;
                    BigDecimal minDouble = MIN_DOUBLE_AS_BIG_DECIMAL;
                    return bd.compareTo(maxDouble) <= 0 && bd.compareTo(minDouble) >= 0;
                } catch (Exception e) {
                    return false;
                }
            }
        }
        return super.isCoercibleTo(targetType, value);
    }

    @Override
    public boolean isSizeCompatible(ImmutableBytesWritable ptr, Object value, PDataType srcType,
            SortOrder sortOrder, Integer maxLength, Integer scale, Integer desiredMaxLength, Integer desiredScale) {
        if (ptr.getLength() == 0) {
            return true;
        }
        // Any numeric type fits into a DECIMAL
        if (srcType != PDecimal.INSTANCE) {
            if(!srcType.isCoercibleTo(this)) {
                throw new IllegalArgumentException(TypeMismatchException.newException(srcType, this));
            }
            return true;
        }
        // Use the scale from the value if provided, as it prevents a deserialization.
        // The maxLength and scale for the underlying expression are ignored, because they
        // are not relevant in this case: for example a DECIMAL(10,2) may be assigned to a
        // DECIMAL(5,0) as long as the value fits.
        if (value != null) {
            BigDecimal v = (BigDecimal) value;
            maxLength = v.precision();
            scale = v.scale();
        } else {
            this.coerceBytes(ptr, value, srcType, maxLength, scale, sortOrder, desiredMaxLength, desiredScale, sortOrder, true);
            int[] v = getDecimalPrecisionAndScale(ptr.get(), ptr.getOffset(), ptr.getLength(), sortOrder);
            maxLength = v[0];
            scale = v[1];
        }
        if (desiredMaxLength != null && desiredScale != null && maxLength != null && scale != null &&
                ((desiredScale == null && desiredMaxLength < maxLength) ||
                        (desiredMaxLength - desiredScale) < (maxLength - scale))) {
            return false;
        }
        return true;
    }

    @Override
    public void coerceBytes(ImmutableBytesWritable ptr, Object object, PDataType actualType,
            Integer maxLength, Integer scale, SortOrder actualModifier, Integer desiredMaxLength, Integer desiredScale,
            SortOrder expectedModifier) {
        if (desiredScale == null) {
            // deiredScale not available, or we do not have scale requirement, delegate to parents.
            super.coerceBytes(ptr, object, actualType, maxLength, scale, actualModifier, desiredMaxLength,
                    desiredScale, expectedModifier);
            return;
        }
        if (ptr.getLength() == 0) {
            return;
        }
        if (scale == null) {
            if (object != null) {
                BigDecimal v = (BigDecimal) object;
                scale = v.scale();
            } else {
                int[] v = getDecimalPrecisionAndScale(ptr.get(), ptr.getOffset(), ptr.getLength(), actualModifier);
                scale = v[1];
            }
        }
        if (this == actualType && scale <= desiredScale) {
            // No coerce and rescale necessary
            return;
        } else {
            BigDecimal decimal;
            // Rescale is necessary.
            if (object != null) { // value object is passed in.
                decimal = (BigDecimal) toObject(object, actualType);
            } else { // only value bytes is passed in, need to convert to object first.
                decimal = (BigDecimal) toObject(ptr);
            }
            decimal = decimal.setScale(desiredScale, BigDecimal.ROUND_DOWN);
            ptr.set(toBytes(decimal));
        }
    }

    @Override
    public Object toObject(String value) {
        if (value == null || value.length() == 0) {
            return null;
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException e) {
            throw newIllegalDataException(e);
        }
    }

    @Override
    public Integer estimateByteSizeFromLength(Integer length) {
        // No association of runtime byte size from decimal precision.
        return null;
    }

    @Override
    public String toStringLiteral(byte[] b, int offset, int length, Format formatter) {
        if (formatter == null) {
            BigDecimal o = (BigDecimal) toObject(b, offset, length);
            return o.toPlainString();
        }
        return super.toStringLiteral(b, offset, length, formatter);
    }

    @Override
    public String toStringLiteral(Object o, Format formatter) {
        if (formatter == null) {
            if(o == null) {
                return String.valueOf(o);
            }
            return ((BigDecimal)o).toPlainString();
        }
        return super.toStringLiteral(o, formatter);
    }

    @Override
    public Object getSampleValue(Integer maxLength, Integer arrayLength) {
        return new BigDecimal((Long) PLong.INSTANCE.getSampleValue(maxLength, arrayLength));
    }

    // take details from org.apache.phoenix.schema.types.PDataType#toBigDecimal(byte[], int, int)
    @Override
    public int signum(byte[] bytes, int offset, int length, SortOrder sortOrder, Integer maxLength,
            Integer scale) {
        byte signByte;
        if (sortOrder == SortOrder.DESC) {
            signByte = SortOrder.invert(bytes[offset]);
        } else {
            signByte = bytes[offset];
        }
        if (length == 1 && signByte == ZERO_BYTE) {
            return 0;
        }
        return ((signByte & 0x80) == 0) ? -1 : 1;
    }

    @Override
    public void abs(byte[] bytes, int offset, int length, SortOrder sortOrder,
            ImmutableBytesWritable outPtr) {
        if (sortOrder == SortOrder.DESC) {
            bytes = SortOrder.invert(bytes, offset, new byte[length], 0, length);
            offset = 0;
        }
        BigDecimal bigDecimal = toBigDecimal(bytes, offset, length);
        outPtr.set(toBytes(bigDecimal.abs()));
    }
}
